#include <stx/imp/cmn.hpp>

#if COMPILER_IS_CLANG
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wold-style-cast"
    #pragma clang diagnostic ignored "-Wsign-conversion"
    #include <stx/imp/murmurhash3.cpp>
    #pragma clang diagnostic pop
#else
    #pragma GCC diagnostic   push
    #pragma GCC diagnostic   ignored "-Wold-style-cast"
    #pragma clang diagnostic ignored "-Wsign-conversion"
    #include <stx/imp/murmurhash3.cpp>
    #pragma GCC diagnostic pop
#endif

inline namespace stx
{
std::size_t base64Decode(std::string_view const sv_b64,
                         void*                  obuf,
                         std::size_t const      obuf_size)
{
    ASSERT(obuf);

    auto output_len = getBase64DecodedSize(sv_b64);
    if (output_len > obuf_size)
    {
        LOG_ERROR("The buffer is too small: "sv, obuf_size, _I_, output_len);
        return 0uz;
    }

    ::base64_decode(
        sv_b64.data(), sv_b64.size(), static_cast<char*>(obuf), &output_len, 0);
    return output_len;
}

Buffer base64Decode(std::string_view const sv_b64)
{
    auto ret = Buffer{};
    resizeDefaultInit(ret, getBase64DecodedSize(sv_b64));

    auto output_len = ret.size();
    ::base64_decode(sv_b64.data(),
                    sv_b64.size(),
                    std::bit_cast<char*>(ret.data()),
                    &output_len,
                    0);
    ASSERT(output_len == ret.size());

    return ret;
}

std::string base64Encode(void const* const data, std::size_t const size)
{
    auto ret = std::string{};
    resizeDefaultInit(ret, getBase64EncodedSize(size));

    auto output_len = ret.size();
    ::base64_encode(
        static_cast<char const*>(data), size, ret.data(), &output_len, 0);
    ASSERT(output_len == ret.size());

    return ret;
}

constexpr bool isPercentEncodingAtom(IsOctet auto const octet) noexcept
{
    auto const c = static_cast<char>(octet);
    return isAlphanumeric(c) || '-' == c || '_' == c || '.' == c || '~' == c;
}

std::string percentEncode(void const* const data,
                          std::size_t const size,
                          SbLowercase const sb_lowercase)
{
    auto const rmm = MemView(data, size);
    auto const ret_size =
        ranges::count_if(rmm, BIND(isPercentEncodingAtom, _1));

    auto ret = std::string{};
    resizeDefaultInit(ret, Number(ret_size) + 3 * (Number(size) - ret_size));

    auto i = 0uz;
    for (auto const c : rmm)
    {
        if (isPercentEncodingAtom(c))
        {
            ret[i++] = static_cast<char>(c);
        }
        else
        {
            ret[i++] = '%';
            ret[i++] = getHighHalfHexChar(c, sb_lowercase);
            ret[i++] = getLowHalfHexChar(c, sb_lowercase);
        }
    }

    return ret;
}

Buffer percentDecode(std::string_view const sv_input)
{
    auto ret      = Buffer{};
    auto buf_size = 0uz;

    auto const sv_size = sv_input.size();

    for (auto i = 0uz; i < sv_size;)
    {
        auto const c = sv_input[i];
        if ('%' == c)
        {
            i += 3;
        }
        else
        {
            ++i;
        }

        ++buf_size;
    }

    ret.reserve(buf_size);

    for (auto i = 0uz; i < sv_size;)
    {
        auto const c = sv_input[i];
        if (isPercentEncodingAtom(c))
        {
            insert(ret, static_cast<std::byte>(c));
            ++i;
            continue;
        }
        else if ('%' == c)
        {
            ASSERT(i + 3 <= sv_size);

            auto const opt = ton(tosv(&sv_input[i + 1], 2), NumberBase::K_16);
            CHECK(opt);

            auto const n = static_cast<std::uint8_t>(*opt);
            insert(ret, static_cast<std::byte>(n));
            i += 3;
            continue;
        }
        else
        {
            RUNTIME_ERROR("Invalid percent-encoded input: "sv, sv_input);
        }
    }

    return ret;
}

Md5 md5(MemView const mv)
{
    return digest<MessageDigestType::K_MD5>(mv.data(), mv.size());
}

Sha1 sha1(MemView const mv)
{
    return digest<MessageDigestType::K_SHA1>(mv.data(), mv.size());
}

Sha224 sha224(MemView const mv)
{
    return digest<MessageDigestType::K_SHA224>(mv.data(), mv.size());
}

Sha256 sha256(MemView const mv)
{
    return digest<MessageDigestType::K_SHA256>(mv.data(), mv.size());
}

Sha384 sha384(MemView const mv)
{
    return digest<MessageDigestType::K_SHA384>(mv.data(), mv.size());
}

Sha512 sha512(MemView const mv)
{
    return digest<MessageDigestType::K_SHA512>(mv.data(), mv.size());
}

Md5 hmacMd5(MemView const mv_key, MemView const mv)
{
    return hmac<MessageDigestType::K_MD5>(mv_key, mv);
}

Sha1 hmacSha1(MemView const mv_key, MemView const mv)
{
    return hmac<MessageDigestType::K_SHA1>(mv_key, mv);
}

Sha224 hmacSha224(MemView const mv_key, MemView const mv)
{
    return hmac<MessageDigestType::K_SHA224>(mv_key, mv);
}

Sha256 hmacSha256(MemView const mv_key, MemView const mv)
{
    return hmac<MessageDigestType::K_SHA256>(mv_key, mv);
}

Sha384 hmacSha384(MemView const mv_key, MemView const mv)
{
    return hmac<MessageDigestType::K_SHA384>(mv_key, mv);
}

Sha512 hmacSha512(MemView const mv_key, MemView const mv)
{
    return hmac<MessageDigestType::K_SHA512>(mv_key, mv);
}

AesGcmIv::AesGcmIv(std::string_view const sv_hex_iv)
{
    this->load(sv_hex_iv);
}

void AesGcmIv::load(std::string_view const sv_hex_iv)
{
    gLastError().clear();

    if (sv_hex_iv.size() != this->size() * 2)
    {
        LOG_ERROR("Invalid hex string size: "sv, sv_hex_iv.size());
    }
    else
    {
        hex2bin(sv_hex_iv, this->data(), this->size());
    }
}

AesGcmTag::AesGcmTag(std::string_view const sv_hex_tag)
{
    this->load(sv_hex_tag);
}

void AesGcmTag::load(std::string_view const sv_hex_tag)
{
    gLastError().clear();

    if (sv_hex_tag.size() != this->size() * 2)
    {
        LOG_ERROR("Invalid hex string size: "sv, sv_hex_tag.size());
    }
    else
    {
        hex2bin(sv_hex_tag, this->data(), this->size());
    }
}

template<AesKeyType K_AES_KEY_TYPE_>
std::optional<AesGcmMsg> decrypt__(AesKey<K_AES_KEY_TYPE_> const& key,
                                   AesGcmMsg const&               ciphertext)
{
    auto up_ctx =
        std::unique_ptr<::EVP_CIPHER_CTX, decltype(&::EVP_CIPHER_CTX_free)>(
            ::EVP_CIPHER_CTX_new(), &::EVP_CIPHER_CTX_free);
    if (!up_ctx)
    {
        LOG_ERROR("::EVP_CIPHER_CTX_new."sv);
        return std::nullopt;
    }

    auto status = 0;
    if (AesKeyType::K_128_BITS == K_AES_KEY_TYPE_)
    {
        status = ::EVP_DecryptInit_ex(
            up_ctx.get(), ::EVP_aes_128_gcm(), nullptr, nullptr, nullptr);
    }
    else if (AesKeyType::K_192_BITS == K_AES_KEY_TYPE_)
    {
        status = ::EVP_DecryptInit_ex(
            up_ctx.get(), ::EVP_aes_192_gcm(), nullptr, nullptr, nullptr);
    }
    else if (AesKeyType::K_256_BITS == K_AES_KEY_TYPE_)
    {
        status = ::EVP_DecryptInit_ex(
            up_ctx.get(), ::EVP_aes_256_gcm(), nullptr, nullptr, nullptr);
    }
    else
    {
        LOG_ERROR("Invalid K_AES_KEY_TYPE_: "sv, K_AES_KEY_TYPE_);
        return std::nullopt;
    }

    if (1 != status)
    {
        LOG_ERROR("::EVP_DecryptInit_ex: "sv, status);
        return std::nullopt;
    }

    auto ret = AesGcmMsg{};
    resizeDefaultInit(ret.data, ciphertext.data.size());
    ret.aad = ciphertext.aad;
    ret.iv  = ciphertext.iv;
    ret.tag = ciphertext.tag;

    status = ::EVP_DecryptInit_ex(
        up_ctx.get(),
        nullptr,
        nullptr,
        std::bit_cast<unsigned char const*>(key.data()),
        std::bit_cast<unsigned char const*>(ret.iv.data()));
    if (1 != status)
    {
        LOG_ERROR("::EVP_DecryptInit_ex: "sv, status);
        return std::nullopt;
    }

    auto len = 0;
    if (!ciphertext.aad.empty())
    {
        status = ::EVP_DecryptUpdate(
            up_ctx.get(),
            nullptr,
            &len,
            std::bit_cast<unsigned char const*>(ciphertext.aad.data()),
            Number(ciphertext.aad.size()));
        if (1 != status)
        {
            LOG_ERROR("::EVP_DecryptUpdate(1): "sv, status);
            return std::nullopt;
        }
    }

    status = ::EVP_DecryptUpdate(
        up_ctx.get(),
        std::bit_cast<unsigned char*>(ret.data.data()),
        &len,
        std::bit_cast<unsigned char const*>(ciphertext.data.data()),
        Number(ciphertext.data.size()));
    if (1 != status)
    {
        LOG_ERROR("::EVP_DecryptUpdate(2): "sv, status);
        return std::nullopt;
    }

    if (std::cmp_not_equal(ret.data.size(), len))
    {
        LOG_ERROR("size mismatch: "sv, ret.data.size(), _I_, len);
        return std::nullopt;
    }

    status = ::EVP_CIPHER_CTX_ctrl(
        up_ctx.get(), EVP_CTRL_GCM_SET_TAG, 16, ret.tag.data());
    if (1 != status)
    {
        LOG_ERROR("::EVP_CIPHER_CTX_ctrl(EVP_CTRL_GCM_SET_TAG): "sv, status);
        return std::nullopt;
    }

    status = ::EVP_DecryptFinal_ex(
        up_ctx.get(),
        std::bit_cast<unsigned char*>(ret.data.data() + len),
        &len);
    if (1 != status)
    {
        LOG_ERROR("::EVP_DecryptFinal_ex: "sv, status);
        return std::nullopt;
    }

    return ret;
}

template<AesKeyType K_AES_KEY_TYPE_>
std::optional<AesGcmMsg> encrypt__(AesKey<K_AES_KEY_TYPE_> const& key,
                                   void const* const              input,
                                   std::size_t const              ilen,
                                   std::string                    aad)
{
    auto up_ctx =
        std::unique_ptr<::EVP_CIPHER_CTX, decltype(&::EVP_CIPHER_CTX_free)>(
            ::EVP_CIPHER_CTX_new(), &::EVP_CIPHER_CTX_free);
    if (!up_ctx)
    {
        LOG_ERROR("::EVP_CIPHER_CTX_new."sv);
        return std::nullopt;
    }

    auto status = 0;
    if (AesKeyType::K_128_BITS == K_AES_KEY_TYPE_)
    {
        status = ::EVP_EncryptInit_ex(
            up_ctx.get(), ::EVP_aes_128_gcm(), nullptr, nullptr, nullptr);
    }
    else if (AesKeyType::K_192_BITS == K_AES_KEY_TYPE_)
    {
        status = ::EVP_EncryptInit_ex(
            up_ctx.get(), ::EVP_aes_192_gcm(), nullptr, nullptr, nullptr);
    }
    else if (AesKeyType::K_256_BITS == K_AES_KEY_TYPE_)
    {
        status = ::EVP_EncryptInit_ex(
            up_ctx.get(), ::EVP_aes_256_gcm(), nullptr, nullptr, nullptr);
    }
    else
    {
        LOG_ERROR("Invalid K_AES_KEY_TYPE_: "sv, K_AES_KEY_TYPE_);
        return std::nullopt;
    }

    if (1 != status)
    {
        LOG_ERROR("::EVP_EncryptInit_ex: "sv, status);
        return std::nullopt;
    }

    auto ret = AesGcmMsg{};
    resizeDefaultInit(ret.data, ilen);
    ret.aad = std::move(aad);
    randomize(ret.iv);

    status = ::EVP_EncryptInit_ex(
        up_ctx.get(),
        nullptr,
        nullptr,
        std::bit_cast<unsigned char const*>(key.data()),
        std::bit_cast<unsigned char const*>(ret.iv.data()));
    if (1 != status)
    {
        LOG_ERROR("::EVP_EncryptInit_ex: "sv, status);
        return std::nullopt;
    }

    auto len = 0;
    if (!ret.aad.empty())
    {
        status = ::EVP_EncryptUpdate(
            up_ctx.get(),
            nullptr,
            &len,
            std::bit_cast<unsigned char const*>(ret.aad.data()),
            Number(ret.aad.size()));
        if (1 != status)
        {
            LOG_ERROR("::EVP_EncryptUpdate(1): "sv, status);
            return std::nullopt;
        }
    }

    status = ::EVP_EncryptUpdate(up_ctx.get(),
                                 std::bit_cast<unsigned char*>(ret.data.data()),
                                 &len,
                                 std::bit_cast<unsigned char const*>(input),
                                 Number(ilen));
    if (1 != status)
    {
        LOG_ERROR("::EVP_EncryptUpdate(2): "sv, status);
        return std::nullopt;
    }

    auto ciphertext_len = len;

    status = ::EVP_EncryptFinal_ex(
        up_ctx.get(),
        std::bit_cast<unsigned char*>(ret.data.data() + len),
        &len);
    if (1 != status)
    {
        LOG_ERROR("::EVP_EncryptFinal_ex: "sv, status);
        return std::nullopt;
    }

    ciphertext_len += len;
    if (ret.data.size() != Number(ciphertext_len))
    {
        LOG_ERROR("size mismatch: "sv, ret.data.size(), _I_, ciphertext_len);
        return std::nullopt;
    }

    status = ::EVP_CIPHER_CTX_ctrl(
        up_ctx.get(), EVP_CTRL_GCM_GET_TAG, 16, ret.tag.data());
    if (1 != status)
    {
        LOG_ERROR("EVP_CIPHER_CTX_ctrl(EVP_CTRL_GCM_GET_TAG): "sv, status);
        return std::nullopt;
    }

    return ret;
}

std::optional<AesGcmMsg> decrypt(AesKey<AesKeyType::K_128_BITS> const& key,
                                 AesGcmMsg const& ciphertext)
{
    return decrypt__(key, ciphertext);
}

std::optional<AesGcmMsg> decrypt(AesKey<AesKeyType::K_192_BITS> const& key,
                                 AesGcmMsg const& ciphertext)
{
    return decrypt__(key, ciphertext);
}

std::optional<AesGcmMsg> decrypt(AesKey<AesKeyType::K_256_BITS> const& key,
                                 AesGcmMsg const& ciphertext)
{
    return decrypt__(key, ciphertext);
}

std::optional<AesGcmMsg> encrypt(AesKey<AesKeyType::K_128_BITS> const& key,
                                 void const* const plaintext,
                                 std::size_t const plaintext_size,
                                 std::string       aad)
{
    return encrypt__(key, plaintext, plaintext_size, std::move(aad));
}

std::optional<AesGcmMsg> encrypt(AesKey<AesKeyType::K_192_BITS> const& key,
                                 void const* const plaintext,
                                 std::size_t const plaintext_size,
                                 std::string       aad)
{
    return encrypt__(key, plaintext, plaintext_size, std::move(aad));
}

std::optional<AesGcmMsg> encrypt(AesKey<AesKeyType::K_256_BITS> const& key,
                                 void const* const plaintext,
                                 std::size_t const plaintext_size,
                                 std::string       aad)
{
    return encrypt__(key, plaintext, plaintext_size, std::move(aad));
}
} // namespace stx